/* Copyright (c) 2001 - 2013 OpenPlans - www.openplans.org. All rights reserved.
 * This code is licensed under the GPL 2.0 license, available at the root
 * application directory.
 */
package org.geoserver.wms.legendgraphic;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.GradientPaint;
import java.awt.Graphics2D;
import java.awt.Paint;
import java.awt.RenderingHints;
import java.awt.geom.Rectangle2D;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.geoserver.wms.legendgraphic.LegendUtils.HAlign;
import org.geoserver.wms.legendgraphic.LegendUtils.VAlign;
import org.geotools.styling.ColorMapEntry;
import org.opengis.style.ColorMap;

/**
 * This class mimics a simple cell for the final {@link ColorMap} legend reprensentation.
 * 
 * @author Simone Giannecchini, GeoSolutions SAS
 * 
 */
@SuppressWarnings("deprecation")
abstract class Cell {
    protected final Color bkgColor;

    protected final double bkgOpacity;

    protected final String text;

    protected final HAlign hAlign;

    protected final VAlign vAlign;

    protected final Dimension requestedDimension;

    protected final Font labelFont;

    protected final Color labelFontColor;

    protected final boolean fontAntiAliasing;

    protected final Color borderColor;

    protected Cell(final Color bkgColor, final double bkgOpacity, final String text,
            final HAlign hAlign, final VAlign vAlign, final Dimension requestedDimension,
            final Font labelFont, final Color labelFontColor, final boolean fontAntiAliasing,
            final Color borderColor) {
        this.bkgColor = bkgColor;
        this.bkgOpacity = bkgOpacity;
        this.text = text;
        this.hAlign = hAlign;
        this.vAlign = vAlign;
        this.requestedDimension = requestedDimension;
        this.labelFont = labelFont;
        this.labelFontColor = labelFontColor;
        this.fontAntiAliasing = fontAntiAliasing;
        this.borderColor = borderColor;
    }

    public abstract void draw(final Graphics2D graphics, final Rectangle2D clipBox,
            final boolean completeBorder);

    /**
     * Retrieves the preferred dimension for this {@link Cell} element within the provided graphics
     * element.
     * 
     * @param graphics
     *            {@link Graphics2D} object to use for computing the preferred dimension
     * @return the preferred dimension for this {@link Cell} element within the provided graphics
     *         element.
     */
    public abstract Dimension getPreferredDimension(final Graphics2D graphics);
}

/**
 * This class mimics a simple row for the final {@link ColorMap} legend representation.
 * 
 * @author Simone Giannecchini, GeoSolutions SAS
 * 
 */
abstract class Row {
    private final List<Cell> cells = new ArrayList<Cell>();

    Row() {
    }

    Row(final List<Cell> columns) {
        columns.addAll(columns);
    }

    protected Cell get(final int index) {
        return cells.get(index);
    }

    protected void add(final Cell cell) {
        cells.add(cell);
    }
}

abstract class ColorMapEntryLegendBuilder extends Row {

    protected ColorMapEntryLegendBuilder() {
        super();
    }

    protected ColorMapEntryLegendBuilder(List<Cell> columns) {
        super(columns);

    }

    protected ColorMapEntryLegendBuilder(final ColorManager colorManager,
            final TextManager labelManager, final TextManager ruleManager) {
        super(Arrays.asList(colorManager, ruleManager, labelManager));

    }

    public boolean hasLabel() {
        return hasLabel;
    }

    protected boolean hasLabel;

    public Cell getRuleManager() {
        return get(1);
    }

    public Cell getLabelManager() {
        return get(2);
    }

    public Cell getColorManager() {
        return get(0);
    }
}

class SingleColorMapEntryLegendBuilder extends ColorMapEntryLegendBuilder {

    @SuppressWarnings("deprecation")
    public SingleColorMapEntryLegendBuilder(final List<ColorMapEntry> cMapEntries,
            final HAlign hAlign, final VAlign vAling, final Color bkgColor,
            final double bkgOpacity, final String text, final Dimension requestedDimension,
            final Font labelFont, final Color labelFontColor, final boolean fontAntiAliasing,
            final Color borderColor) {

        final ColorMapEntry currentCME = cMapEntries.get(0);
        Color color = LegendUtils.color(currentCME);
        final double opacity = LegendUtils.getOpacity(currentCME);
        color = new Color(color.getRed(), color.getGreen(), color.getBlue(), (int) (255 * opacity));
        super.add(new SimpleColorManager(color, opacity, requestedDimension, borderColor));

        final String label = currentCME.getLabel();
        final double quantity = LegendUtils.getQuantity(currentCME);
        final String symbol = " = ";
        String rule = Double.toString(quantity) + " " + symbol + " x";

        super.add(new TextManager(rule, vAling, hAlign, bkgColor, requestedDimension, labelFont,
                labelFontColor, fontAntiAliasing, borderColor));

        // add the label the label to the rule so that we draw all text just once
        if (label != null) {

            hasLabel = true;
            super.add(new TextManager(label, vAling, hAlign, bkgColor, requestedDimension,
                    labelFont, labelFontColor, fontAntiAliasing, borderColor));
        } else
            super.add(null);
    }

}

class RampColorMapEntryLegendBuilder extends ColorMapEntryLegendBuilder {

    @SuppressWarnings("deprecation")
    public RampColorMapEntryLegendBuilder(final List<ColorMapEntry> mapEntries,
            final HAlign hAlign, final VAlign vAling, final Color bkgColor,
            final double bkgOpacity, final String text, final Dimension requestedDimension,
            final Font labelFont, final Color labelFontColor, final boolean fontAntiAliasing,
            final Color borderColor) {

        final ColorMapEntry previousCME = mapEntries.get(0);
        final ColorMapEntry currentCME = mapEntries.get(1);
        boolean leftEdge;
        if (previousCME == null)
            leftEdge = true;
        else
            leftEdge = false;

        Color previousColor;
        if (!leftEdge) {
            previousColor = LegendUtils.color(previousCME);
            final double opacity = LegendUtils.getOpacity(previousCME);
            previousColor = new Color(previousColor.getRed(), previousColor.getGreen(),
                    previousColor.getBlue(), (int) (255 * opacity + 0.5));
        } else
            previousColor = null;

        Color color = LegendUtils.color(currentCME);
        double opacity = LegendUtils.getOpacity(currentCME);
        color = new Color(color.getRed(), color.getGreen(), color.getBlue(), (int) (255 * opacity));
        super.add(new GradientColorManager(color, opacity, previousColor, requestedDimension,
                borderColor));

        String label = currentCME.getLabel();
        double quantity = LegendUtils.getQuantity(currentCME);

        String symbol = leftEdge ? " > " : " = ";
        String rule = leftEdge ? Double.toString(quantity) + " " + symbol + " x" : Double
                .toString(quantity)
                + " " + symbol + " x";

        super.add(new TextManager(rule, vAling, hAlign, bkgColor, requestedDimension, labelFont,
                labelFontColor, leftEdge, borderColor));

        // add the label the label to the rule so that we draw all text just once
        if (label != null) {

            hasLabel = true;
            super.add(new TextManager(label, vAling, hAlign, bkgColor, requestedDimension,
                    labelFont, labelFontColor, leftEdge, borderColor));
        } else
            super.add(null);

    }

}

class ClassesEntryLegendBuilder extends ColorMapEntryLegendBuilder {

    @SuppressWarnings("deprecation")
    public ClassesEntryLegendBuilder(

    final List<ColorMapEntry> mapEntries, final HAlign hAlign, final VAlign vAling,
            final Color bkgColor, final double bkgOpacity, final String text,
            final Dimension requestedDimension, final Font labelFont, final Color labelFontColor,
            final boolean fontAntiAliasing, final Color borderColor) {

        final ColorMapEntry previousCME = mapEntries.get(0);
        final ColorMapEntry currentCME = mapEntries.get(1);
        boolean leftEdge;
        if (previousCME == null)
            leftEdge = true;
        else
            leftEdge = false;

        Color color = LegendUtils.color(currentCME);
        final double opacity = LegendUtils.getOpacity(currentCME);
        color = new Color(color.getRed(), color.getGreen(), color.getBlue(), (int) (255 * opacity));
        super.add(new SimpleColorManager(color, opacity, requestedDimension, borderColor));

        String label = currentCME.getLabel();
        double quantity1 = leftEdge ? LegendUtils.getQuantity(currentCME) : LegendUtils
                .getQuantity(previousCME);
        double quantity2 = LegendUtils.getQuantity(currentCME);
        String symbol1 = null, symbol2 = null;
        if (leftEdge)
            symbol1 = " < ";
        else {
            symbol1 = " <= ";
            symbol2 = " < ";
        }
        String rule = leftEdge ? Double.toString(quantity1) + " " + symbol1 + " x" : Double
                .toString(quantity1)
                + " " + symbol1 + " x " + symbol2 + " " + Double.toString(quantity2);

        super.add(new TextManager(rule, vAling, hAlign, bkgColor, requestedDimension, labelFont,
                labelFontColor, leftEdge, borderColor));

        // add the label the label to the rule so that we draw all text just once
        if (label != null) {

            hasLabel = true;
            super.add(new TextManager(label, vAling, hAlign, bkgColor, requestedDimension,
                    labelFont, labelFontColor, leftEdge, borderColor));
        } else
            super.add(null);

    }
}

/**
 * This class mimics a simple color cell for the final {@link ColorMap} legend representation. It is
 * responsible for for drawing colors for a {@link ColorMapEntry}.
 * 
 * @author Simone Giannecchini, GeoSolutions SAS
 * 
 */
abstract class ColorManager extends Cell {

    public ColorManager(final Color color, final double opacity,
            final Dimension requestedDimension, final Color borderColor) {
        super(color, opacity, null, null, null, requestedDimension, null, null, false, borderColor);
    }

    public abstract void draw(final Graphics2D graphics, final Rectangle2D clipBox,
            final boolean completeBorder);

    @Override
    public Dimension getPreferredDimension(final Graphics2D graphics) {
        return new Dimension(requestedDimension);
    }

}

class SimpleColorManager extends ColorManager {

    public SimpleColorManager(final Color color, final double opacity,
            final Dimension requestedDimension, final Color borderColor) {
        super(color, opacity, requestedDimension, borderColor);
    }

    @Override
    public void draw(final Graphics2D graphics, final Rectangle2D clipBox,
            final boolean completeBorder) {
        // bkgColor fill
        if (bkgOpacity > 0) {
            // OPAQUE
            final Color oldColor = graphics.getColor();
            final Color newColor = new Color(bkgColor.getRed(), bkgColor.getGreen(), bkgColor
                    .getBlue(), (int) (255 * bkgOpacity + 0.5));
            graphics.setColor(newColor);
            graphics.fill(clipBox);
            // make bkgColor customizable
            graphics.setColor(borderColor);
            if (completeBorder) {

                final int minx = (int) (clipBox.getMinX() + 0.5);
                final int miny = (int) (clipBox.getMinY() + 0.5);
                final int w = (int) (clipBox.getWidth() + 0.5) - 1;
                final int h = (int) (clipBox.getHeight() + 0.5) - 1;
                graphics.draw(new Rectangle2D.Double(minx, miny, w, h));
            }
            // restore bkgColor
            graphics.setColor(oldColor);
        } else {
            // TRANSPARENT
            final Color oldColor = graphics.getColor();

            // white background
            graphics.setColor(Color.white);
            graphics.fill(clipBox);

            // now the red cross
            graphics.setColor(Color.RED);
            final int minx = (int) (clipBox.getMinX() + 0.5);
            final int miny = (int) (clipBox.getMinY() + 0.5);
            final int maxx = (int) (minx + clipBox.getWidth() - 1 + 0.5);
            final int maxy = (int) (miny + clipBox.getHeight() - 1 + 0.5);
            graphics.drawLine(minx, miny, maxx, maxy);
            graphics.drawLine(minx, maxy, maxx, miny);

            graphics.setColor(borderColor);
            if (completeBorder) {

                final int w = (int) (clipBox.getWidth() + 0.5) - 1;
                final int h = (int) (clipBox.getHeight() + 0.5) - 1;
                graphics.draw(new Rectangle2D.Double(minx, miny, w, h));
            }

            // restore bkgColor
            graphics.setColor(oldColor);
        }

    }

}

class GradientColorManager extends SimpleColorManager {

    @Override
    public Dimension getPreferredDimension(Graphics2D graphics) {
        // twice as much space for the Height to account for the gradient
        return new Dimension(requestedDimension.width,
                (int) (1.5 * requestedDimension.height + 0.5));
    }

    private Color previousColor = null;

    private boolean leftEdge;

    public GradientColorManager(final Color color, final double opacity, final Color previousColor,
            final Dimension requestedDimension, final Color borderColor) {
        super(color, opacity, requestedDimension, borderColor);
        this.previousColor = previousColor;
        if (previousColor == null)
            leftEdge = true;
    }

    @Override
    public void draw(final Graphics2D graphics, final Rectangle2D clipBox,
            final boolean completeBorder) {

        // getting clipbox dimensions
        final double minx = clipBox.getMinX();
        final double miny = clipBox.getMinY();
        final double w = clipBox.getWidth();
        final double h = clipBox.getHeight();

        // GRADIENT
        if (!leftEdge) {
            // rectangle for the gradient
            final Rectangle2D.Double rectLegend = new Rectangle2D.Double(minx, miny, w, h / 2);

            // gradient paint
            final Paint oldPaint = graphics.getPaint();
            final GradientPaint paint = new GradientPaint((float) minx, (float) miny,
                    previousColor, (float) minx, (float) (miny + h / 2), bkgColor);

            // do the magic
            graphics.setPaint(paint);
            graphics.fill(rectLegend);

            // restore paint
            graphics.setPaint(oldPaint);

        }

        // COLOR BOX
        // careful with handling the leftEdge case
        final Rectangle2D rectLegend = new Rectangle2D.Double(minx, miny + (leftEdge ? 0 : h / 2),
                w, !leftEdge ? h / 2 : h);
        super.draw(graphics, rectLegend, completeBorder);
        if (completeBorder) {
            final Color oldColor = graphics.getColor();
            // make bkgColor customizable
            graphics.setColor(borderColor);
            final int minx_ = (int) (clipBox.getMinX() + 0.5);
            final int maxx = (int) (minx + clipBox.getWidth() + 0.5) - 1;
            final int maxy = (int) (miny + clipBox.getHeight() + 0.5) - 1;
            graphics.drawLine(minx_, maxy, maxx, maxy);
            // restore bkgColor
            graphics.setColor(oldColor);
        }

    }

}

/**
 * This class mimics a simple text cell for the final {@link ColorMap} legend representation.
 * 
 * @author Simone Giannecchini, GeoSolutions SAS
 * 
 */
class TextManager extends Cell {

    public TextManager(final String text, final VAlign vAlign, final HAlign hAlign,
            final Color bkgColor, final Dimension requestedDimension, final Font labelFont,
            final Color labelFontColor, final boolean fontAntiAliasing, final Color borderColor) {
        super(bkgColor, 1.0, text, hAlign, vAlign, requestedDimension, labelFont, labelFontColor,
                fontAntiAliasing, borderColor);
    }

    @Override
    public Dimension getPreferredDimension(final Graphics2D graphics) {
        // get old font
        final Font oldFont = graphics.getFont();

        // set new font
        graphics.setFont(labelFont);
        // computing label dimension and creating buffered image on which we can draw the label on
        // it
        final int labelHeight = (int) Math.ceil(graphics.getFontMetrics().getStringBounds(text,
                graphics).getHeight());
        final int labelWidth = (int) Math.ceil(graphics.getFontMetrics().getStringBounds(text,
                graphics).getWidth());
        // restore the old font
        graphics.setFont(oldFont);
        return new Dimension(labelWidth, labelHeight);
    }

    public void draw(final Graphics2D graphics, final Rectangle2D clipBox,
            final boolean completeBorder) {

        // save old font
        final Font oldFont = graphics.getFont();

        // set font and font color and the antialising
        graphics.setColor(labelFontColor);
        graphics.setFont(labelFont);
        if (fontAntiAliasing)
            graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
                    RenderingHints.VALUE_TEXT_ANTIALIAS_ON);

        // Halign==center vAlign==bottom
        final double minx = clipBox.getMinX();
        final double miny = clipBox.getMinY();
        final double w = clipBox.getWidth();
        final double h = clipBox.getHeight();
        final Dimension dimension = getPreferredDimension(graphics);
        // where do we draw?
        final int xText;
        switch (hAlign) {
        case CENTERED:
            xText = (int) (minx + (w - dimension.getWidth()) / 2.0 + 0.5);
            break;
        case LEFT:
            xText = (int) (minx + 0.5);
            break;
        case RIGHT:
            xText = (int) (minx + (w - dimension.getWidth()) + 0.5);
            break;
        case JUSTIFIED:
            throw new UnsupportedOperationException("Unsupported");
        default:
            throw new IllegalStateException("Unsupported horizontal alignment " + hAlign);
        }

        final int yText;
        switch (vAlign) {
        case BOTTOM:
            yText = (int) (miny + h - graphics.getFontMetrics().getDescent() + 0.5);
            break;
        case TOP:
            yText = (int) (miny + graphics.getFontMetrics().getHeight() + 0.5);
            break;
        case MIDDLE:
            yText = (int) (miny + (h + graphics.getFontMetrics().getHeight()) / 2 + 0.5);
            break;
        default:
            throw new IllegalStateException("Unsupported vertical alignment " + vAlign);
        }
        // draw
        graphics.drawString(text, xText, yText);

        // restore the old font
        graphics.setFont(oldFont);

    }
}
